package com.explorex.infra.puma.gateway.netty.ws.handler;

import com.explorex.infra.puma.gateway.netty.common.AbstractBaseServer;
import com.explorex.infra.puma.gateway.netty.common.ChannelManager;
import com.explorex.infra.puma.gateway.netty.ws.BaseHttpResource;
import com.explorex.infra.puma.gateway.netty.ws.WebSocketEvent;
import com.explorex.infra.puma.gateway.netty.ws.WebSocketSession;
import com.explorex.infra.puma.gateway.netty.ws.utils.SocketUtil;
import com.explorex.infra.puma.gateway.netty.ws.utils.UrlParamsUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.CharsetUtil;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.Base64;
import java.util.Map;
import java.util.regex.Pattern;

import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;

/**
 * @author: JacceYang
 * @createTime: 2022/07/15 1:15 下午
 * @description: 消息鉴权处理类
 */
@Slf4j
public class HttpServerHandler extends SimpleChannelInboundHandler<Object> {
    public static final AttributeKey<String> WEBSOCKET_KEY = AttributeKey.valueOf("websocket");

    public static final AttributeKey<String> COOKIE_KEY = AttributeKey.valueOf("cookie");

    private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile(
            "^.*(?:connection.*(?:reset|closed|abort|broken|timed)|broken.*pipe).*$", Pattern.CASE_INSENSITIVE);
    private final BaseHttpResource httpResource;
    private final AbstractBaseServer baseServer;
    private WebSocketServerHandshaker handshaker;
    @Setter
    private WebSocketEvent webSocketEvent;
    @Setter
    private String webSocketPath;

    public HttpServerHandler(AbstractBaseServer baseServer, BaseHttpResource httpResource) {
        this.baseServer = baseServer;
        this.httpResource = httpResource;
    }

    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        // Generate an error page if response getStatus code is not OK (200).
        int statusCode = res.status().code();
        if (statusCode != HttpResponseStatus.OK.code() && res.content().readableBytes() == 0) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        HttpUtil.setContentLength(res, res.content().readableBytes());

        // Send the response and close the connection if necessary.
        if (!HttpUtil.isKeepAlive(req) || statusCode != HttpResponseStatus.OK.code()) {
            res.headers().set(CONNECTION, CLOSE);
            ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
        } else {
            res.headers().set(CONNECTION, CLOSE);
            if (req.protocolVersion().equals(HTTP_1_0)) {
                res.headers().set(CONNECTION, KEEP_ALIVE);
            }
            ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if (webSocketEvent != null) {
            if (ctx.channel().hasAttr(WEBSOCKET_KEY)) {
                webSocketEvent.onCloseEvent(baseServer,
                        (WebSocketSession) ChannelManager.getInstance().getSocketSession(ctx.channel()));
            }
        }
        super.channelInactive(ctx);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (ignoreException(cause)) {
            return;
        }
        log.error("HttpServerHandler exception ", cause);
        ctx.close();
    }

    private boolean ignoreException(Throwable t) {
        if (t instanceof IOException) {
            String message = String.valueOf(t.getMessage()).toLowerCase();
            // first try to match connection reset / broke peer based on the regex. This is the fastest way
            // but may fail on different jdk impls or OS's
            return IGNORABLE_ERROR_MESSAGE.matcher(message).matches();
        }
        return false;
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {

        try {
            log.info("handleHttpRequest called,req={}", req.toString());
            // Handle a bad request.
            if (!req.decoderResult().isSuccess()) {
                sendHttpResponse(ctx, req,
                        new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
                return;
            }

            // Only allow  GET methods.
            if (req.method() != HttpMethod.GET) {
                sendHttpResponse(ctx, req,
                        new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN));
                return;
            }

            // Parse Uri
            String uri = req.uri();
            int index = uri.indexOf("?");
            String parameterStr = "";
            String path = null;
            if (index == -1) {
                path = uri;
            } else {
                path = uri.substring(0, index);
                parameterStr = uri.substring(index + 1);
            }

            WebSocketSession webSocketSession = (WebSocketSession) ChannelManager.getInstance().getSocketSession(ctx.channel());
            if (webSocketPath != null && webSocketPath.trim().length() > 0 && webSocketPath.equals(path)) {
                // Handshake
                WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                        SocketUtil.getWebSocketLocation(ctx.pipeline(), req, webSocketPath), null, true, 5 * 1024 * 1024);
                handshaker = wsFactory.newHandshaker(req);
                if (handshaker == null) {
                    WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
                } else {
                    HttpHeaders responseHeaders = new DefaultHttpHeaders();
                    responseHeaders.add("key", Base64.getEncoder().encodeToString(webSocketSession.keyPair().getPublic().getEncoded()));
                    handshaker.handshake(ctx.channel(), req, responseHeaders, ctx.channel().newPromise());
                    if (!ctx.channel().hasAttr(WEBSOCKET_KEY)) {
                        Attribute<String> attr = ctx.channel().attr(WEBSOCKET_KEY);
                        attr.set("");
                    }

                    if (req.headers().contains(COOKIE_KEY.name())) {
                        ctx.channel().attr(COOKIE_KEY).set(parseSessionId(req.headers()));
                    }

                    if (webSocketEvent != null) {
                        Map<String, Object> parameter = UrlParamsUtil.getUrlParams(parameterStr);
                        webSocketEvent.onOpenEvent(baseServer, webSocketSession, req.headers(), parameter);
                    }
                }
            } else {
                if (httpResource != null) {
                    String resFileName = path;
                    ByteBuf res = httpResource.buildWebSocketRes(req, resFileName);
                    if (null == res) {
                        sendHttpResponse(ctx, req,
                                new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND));
                    } else {
                        sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
                                res));
                    }
                }
            }
        } catch (Exception e) {
            log.error("handleHttpRequest exception ", e);
        }

    }
    private String parseSessionId(HttpHeaders headers){
        String cookieStr = headers.get(COOKIE_KEY.name());
        String[] split = cookieStr.split(";");
        for (String s : split) {
            String[] keyValue = s.split("=");
            String key=keyValue[0].trim();
            if (key.equals("web_session")){
                return keyValue[1].trim();
            }
        }
        return StringUtils.EMPTY;
    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        try {
            WebSocketSession webSocketSession = (WebSocketSession) ChannelManager.getInstance().getSocketSession(ctx.channel());
            if (frame instanceof CloseWebSocketFrame) {
                log.info("channel for receive a close frame from client session={}",webSocketSession);
                handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
                return;
            }
            if (frame instanceof PingWebSocketFrame) {
                ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
                return;
            }
            if (frame instanceof TextWebSocketFrame) {
                if (webSocketEvent != null) {
                    webSocketEvent.onMessageStringEvent(baseServer, webSocketSession, ((TextWebSocketFrame) frame).text());
                }
                return;
            }

            if (frame instanceof BinaryWebSocketFrame) {
                if (webSocketEvent != null) {
                    webSocketEvent.onMessageBinaryEvent(baseServer, webSocketSession, frame.content());
                }
            }
        } catch (Exception e) {
            log.error("handleWebSocketFrame exception", e);
        }
    }

}